package net;

import exceptions.TransferCollapsedException;
import interfaces.Core_Events;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketTimeoutException;
import java.nio.BufferOverflowException;

/**
 *
 * @author Groš
 */

/** Second part logic - fucused on client logic */
public class UDP_Client {

    /** position where to store packet in a packet buffer */
    private int buf_offset = 0;
    private int lastAckID = 0; // last received
    private int ackID = 0; // current received
    private int pID = 0;
    private long bytesSent = 0;
    private int tryes = 0;
    private int step;
    
    private byte[] ackbuf = new byte[core.Core.DATAGRAM_LENGTH];
    private DatagramPacket ackPacket = new DatagramPacket(ackbuf, ackbuf.length);
    
    private byte[][] s_buffer_b = new byte[core.Core.WINDOW_LENGTH][core.Core.DATAGRAM_LENGTH];
    
    /** packet buffer for packets that will be sent */
    private DatagramPacket[] s_buffer = new DatagramPacket[core.Core.WINDOW_LENGTH];
    
    private DatagramSocket socket;
    
    private Core_Events event;
    
    private boolean stop = false;
    private boolean receivedACK = false;
    
    /** Second part logic - fucused on client logic
     * @param socket socket used for sending packets
     * @param event needed interface to communicate with <code>Core</code>
     */
    public UDP_Client(DatagramSocket socket, Core_Events event) {
        this.socket = socket;
        this.event = event;
        initPackets();
    }
    
    /** create new packet buffer with pre-prepared packets */
    private void initPackets() {
        for (int i=0; i < s_buffer.length; i++) {
            s_buffer[i] = new DatagramPacket(s_buffer_b[i], 0, core.Core.DATAGRAM_LENGTH);
        }
    }
    
    /** send packet to a server
     * @param packet packet to send
     * @param flush if immediately send
     * @throws IOException
     * @throws TransferCollapsedException
     */
    public void sendPacket(DatagramPacket packet, boolean flush) 
            throws IOException, TransferCollapsedException 
    {
        // only re-send buffer
        if (packet == null && flush)
            ; // do nothing only flush the buffer
        
        else {
            addToBuffer(packet, buf_offset++);
            if (!isBufferFull() && !flush)
                return;
        }
        
        // send buffer
        for(int i=0; i < buf_offset && !stop; i++) {
            
            // simulation errors on network - 20% packets will be lost
            if (Math.random()*10 > 2) {
                socket.send(s_buffer[i]);
                bytesSent += s_buffer[i].getLength();

                event.core_message("UDP:C>Sending-Datagram ID: " + (UDP_Core.udp_getPacketID(s_buffer[i])) + 
                        " length=" + s_buffer[i].getLength() + "; Destination: IP=" + s_buffer[i].getAddress().getHostAddress() + 
                        " port=" + s_buffer[i].getPort());

                event.core_message("UDP:CD>" + bytesSent);
            }
            else {
                event.core_message("UDP:C>NOT Sending-Datagram ID: " + (UDP_Core.udp_getPacketID(s_buffer[i])));
            }
        }
        
        // try to receive acknoledge from server
        receiveACK(ackPacket);
        
        if (tryes == core.Core.RETRANSMIT_COUNTER)
                throw new TransferCollapsedException();
        
        // if no acknoledge received
        if (!receivedACK) {
            tryes++;
            event.core_message("UDP:C>NOT received ACK");
            
            // try resend same packets in a buffer
            sendPacket(null, true);
            return;
        }
        else {
            
            // if immadiately send - if acknoledge ID is not same as last packet in a buffer
            if (flush && UDP_Core.udp_getPacketID(s_buffer[buf_offset-1]) != ackID) {
                // re-send
                sendPacket(null, true);
                return;
            }
            
            if (ackID >= 0) // transfer in progress
                if (ackID == lastAckID){  // if must resend whole buffer
                    sendPacket(null, true);
                    return;
                }
                if (ackID > lastAckID) { // new good acknoledge received
                    step = UDP_Core.udp_getPacketID(s_buffer[buf_offset-1]);           
                    moveBuffer(ackID + core.Core.WINDOW_LENGTH - step);
                }
            
            if (ackID == UDP_Core.ACK.FIN.value()) // transfer success
                event.core_message("UDP:C>Transfer finished");   
        }  
    }
    
    /** stop process for sendind packets 
     * @param stop true - stop
     */
    public void setStop(boolean stop) {
        this.stop = stop;
        bytesSent = 0;
    }
    
    /** try to receive acknoledge from server
     * @param packet source packet for receive acknoledge
     * @throws IOException
     * @throws TransferCollapsedException
     */
    private void receiveACK(DatagramPacket packet) 
            throws IOException, TransferCollapsedException
    {
        int retransmitCounter = 0;
        receivedACK = false;
        
        // try <code>RETRANSMIT_COUNTER</code> times wait for acknoledge
        while (retransmitCounter < core.Core.RETRANSMIT_COUNTER && !receivedACK) {
            try {
                socket.receive(packet);
                receivedACK = true;
                pID = UDP_Core.udp_getPacketID(packet);
                if (ackID <= pID || pID == UDP_Core.ACK.FIN.value()){
                    lastAckID = ackID;
                    ackID = pID;
                    event.core_message("UDP:C>Received ACK=" + ackID);
                }
            }
            catch (SocketTimeoutException stex) { ++retransmitCounter; continue; }
        }
    }
    
    /** check if the buffer is full
     * @return boolean
     */
    private boolean isBufferFull() {
        return (buf_offset == s_buffer.length) ? true : false;
    }
    
    /** add packet to buffer
     * @param packet source
     * @param index position in buffer
     */
    private void addToBuffer(DatagramPacket packet, int index){
        if(index < 0 || index >= s_buffer.length)
            throw new BufferOverflowException();
        
        copyPacket(packet, s_buffer[index]);
    }
    
    /** move packets in a packet buffer
     * @param leftSteps - about how many fields move
     */
    private void moveBuffer(int leftSteps) {
        buf_offset = core.Core.WINDOW_LENGTH - leftSteps;
        for(int i = 0; i < s_buffer.length; i++)
            if (i + leftSteps < s_buffer.length) {
                copyPacket(s_buffer[i + leftSteps], s_buffer[i]);
            }
    }
    
    /** clones packet 
     * @param source  source packet
     * @param dest destination packet
     */
    private void copyPacket(DatagramPacket source, DatagramPacket dest) {
        dest.setData(source.getData().clone());
        dest.setLength(source.getLength());
        dest.setAddress(source.getAddress());
        dest.setPort(source.getPort());
    }
}
